package pl.edu.pw.polygen.db.dao.generic;

import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.criterion.DetachedCriteria;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 * Hibernate based implementation of a <tt>GenericDao</tt> interface. It serves
 * as a base class for all other DAOs. You should only need to extend this class
 * when your require custom CRUD logic. In other cases this class may be used
 * directly.
 * 
 * <p>
 * To register this class in your Spring context file, use the following XML.
 * 
 * <pre>
 *      &lt;bean id=&quot;fooDao&quot; class=&quot;eu.sii.pl.dao.hibernate.GenericDaoHibernate&quot;&gt;
 *          &lt;constructor-arg value=&quot;eu.sii.pl.model.Foo&quot;/&gt;
 *          &lt;property name=&quot;sessionFactory&quot; ref=&quot;sessionFactory&quot;/&gt;
 *      &lt;/bean&gt;
 * </pre>
 * 
 * </p>
 * 
 * @param <T> persistent type class
 * @param <PK> primary key for the persistent type
 * 
 */
@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
public class GenericDaoImpl<T, PK extends Serializable> extends HibernateDaoSupport implements GenericDao<T, PK> {

    /**
     * Log variable for all ch ild classes.
     */
    protected final Log log = LogFactory.getLog(getClass());

    /**
     * Persistent type class.
     */
    private Class<T> persistentClass;

    /**
     * Constructor that takes in a class to see which type of entity to persist.
     * 
     * @param persistentClass the class type to persist
     */
    public GenericDaoImpl(final Class<T> persistentClass) {
        this.persistentClass = persistentClass;
    }

    /**
     * Constructor for classes extending this one. It determines the type of
     * entity to persist based on the subclass declaration.
     */
    @SuppressWarnings("unchecked")
    protected GenericDaoImpl() {
        this.persistentClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public PK save(T o) {
        // TODO: Thing about merge
        return (PK) getHibernateTemplate().save(o);
    }

    /**
     * {@inheritDoc}
     */
    public void update(T o) {
        getHibernateTemplate().update(o);
    }

    /**
     * {@inheritDoc}
     */
    public void saveOrUpdate(T o) {
        getHibernateTemplate().saveOrUpdate(o);
    }

    /**
     * {@inheritDoc}
     */
    public void delete(PK id) {
        delete(this.getById(id));
    }

    /**
     * {@inheritDoc}
     */
    public void delete(T o) {
        getHibernateTemplate().delete(o);
    }

    /**
     * {@inheritDoc}
     */
    public T getById(PK id) {
        return getHibernateTemplate().get(this.persistentClass, id);
    }

    /**
     * {@inheritDoc} This implementation checks the existence of an object by
     * simply retrieving it.
     */
    public boolean exists(PK id) {
        T entity = getHibernateTemplate().get(this.persistentClass, id);
        return entity != null;
    }

    /**
     * {@inheritDoc}
     */
    public List<T> getAll() {
        return getHibernateTemplate().loadAll(this.persistentClass);
    }



    /**

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public List<T> findByQuery(String query) {
        return getHibernateTemplate().find(query);
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public List<T> findByNamedQuery(String queryName, Map<String, Object> queryParams) {
        String[] params = new String[queryParams.size()];
        Object[] values = new Object[queryParams.size()];

        int index = 0;
        for (String s : queryParams.keySet()) {
            params[index] = s;
            values[index++] = queryParams.get(s);
        }

        return getHibernateTemplate().findByNamedQueryAndNamedParam(queryName, params, values);
    }

    /**
     * Creates and returns Criteria for the <tt>persistentClass</tt>.
     * 
     * @param session - current session
     * @return Hibernate Criteria instance
     */
    protected Criteria createCriteria(Session session) {
        return session.createCriteria(this.persistentClass);
    }

    /**
     * Creates and returns Criteria for the <tt>persistentClass</tt>.
     * 
     * @return Hibernate Criteria instance
     */
    protected DetachedCriteria createDetachedCriteria() {
        return DetachedCriteria.forClass(this.persistentClass);
    }

    /**
     * Retrieves a single object based on a <tt>criteria</tt> passed as an
     * argument.
     * 
     * @param criteria Hibernate Criteria instance
     * @return Single object or <tt>null</tt> if none found
     * @throws HibernateException - if there is more than one matching result
     */
    @SuppressWarnings("unchecked")
    protected T getObjectByCriteria(final DetachedCriteria criteria) {
        // return (T)criteria.uniqueResult();
        // we execute criteria through hibernate template to get consistent
        // spring only exceptions stack
        return (T) getHibernateTemplate().execute(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                return criteria.getExecutableCriteria(session).uniqueResult();
            }
        });
    }

    /**
     * Retrieves a list of objects based on a <tt>criteria</tt> passed as an
     * argument.
     * 
     * @param criteria Hibernate Criteria instance
     * @return List of object matching the criteria
     */
    @SuppressWarnings("unchecked")
    protected List<T> getListByCriteria(final DetachedCriteria criteria) {
        // return (List<T>)criteria.list();
        // we execute criteria through hibernate template to get consistent
        // spring only exceptions stack
        return (List<T>) getHibernateTemplate().execute(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                return criteria.getExecutableCriteria(session).list();
            }
        });
    }

}
